package widgets;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;

/**
 * This class is a replacement for {@link JTabbedPane}.
 * 
 * @author Joshua Tessier
 * @author Neil Dickson
 */
public class CustomTabPane extends JComponent implements MouseWheelListener {
	private ArrayList<TabPage>	pages;
	private int					selectedIndex;
	private int					xOffset;
	private int					totalTabLength;

	private static final int	TAB_HEIGHT		= 20;
	private static final int	MIN_TAB_WIDTH	= 100;
	private static final int	OVERLAP			= 8;
	private static final int	GAP_HEIGHT		= 5;

	private static final int[]	LEFT_CURVE_Y	= new int[] {0,1,1,2,2,3,4,5,6,8,11,14,17,18,19,19,20};
	private static final int[]	RIGHT_CURVE_Y	= new int[] {20,20,20,19,19,18,17,15,12,8,4,0};

	private static final Color	BACKGROUND_COLOUR			= new Color(232,232,232);
	private static final Color	SELECTED_TOP_COLOUR			= new Color(20,20,20);//new Color(18,34,70);
	private static final Color	SELECTED_BOTTOM_COLOUR		= new Color(60,60,60);//new Color(120,130,150);
	private static final Color	DESELECTED_TOP_COLOUR		= new Color(148,148,148);
	private static final Color	DESELECTED_BOTTOM_COLOUR	= new Color(232,232,232);
	private static final Color	GAP_TOP_COLOUR				= SELECTED_BOTTOM_COLOUR;
	private static final Color	GAP_BOTTOM_COLOUR			= new Color(70,70,70);//new Color(160,170,190);
	private static final Color	FONT_COLOUR					= Color.WHITE;
	private static final Color	EDGE_COLOUR					= new Color(148,148,148);
	private static final Font	FONT						= new Font("Arial",Font.PLAIN,12);


	public CustomTabPane() {
		setLayout(null);
		pages = new ArrayList<TabPage>();
		selectedIndex = -1;
		totalTabLength = -OVERLAP;
		setBackground(BACKGROUND_COLOUR);
		addMouseWheelListener(this);
	}

	/**
	 * This is the code used to add a new tab with a name and a component
	 * 
	 * @param name
	 * @param component
	 */
	public void addTab(String name, Component component) {
		addTab(name, null, component, "", true);
	}

	/**
	 * This is the code used to add a new tab with a name, component and an icon.
	 * 
	 * @param name
	 * @param component
	 * @param icon
	 */
	public void addTab(String name, ImageIcon icon, Component component) {
		addTab(name, icon, component, "", true);
	}

	/**
	 * This is the code used to add a tab with a name, tooltip, component and icon
	 * 
	 * @param name
	 * @param toolTip
	 * @param component
	 * @param icon
	 * @param autoSelect
	 */
	public void addTab(String name, ImageIcon icon, Component component, String toolTip, boolean autoSelect) {
		addTabAt(name,toolTip,component,icon,getTabCount(),autoSelect);
	}

	/**
	 * This will add a new tab at a specified index, shifting all other tabs over by one.
	 * @param name
	 * @param toolTip
	 * @param component
	 * @param icon
	 * @param index
	 * @param autoSelect
	 */
	public void addTabAt(String name, String toolTip, Component component, ImageIcon icon, int index, boolean autoSelect) {
		//Do not allow null components
		if ((component != null) && (pages != null)) {
			final TabPage newPage = new TabPage(name,toolTip,component,icon, createATab(name,icon));
			ArrayList<TabPage> newPages = new ArrayList<TabPage>();
			for (int i=0;i< index;i++) {
				newPages.add(pages.get(i));
			}
			newPages.add(newPage);
			newPage.getTab().addActionListener(new ActionListener(){
				public void actionPerformed(ActionEvent e) {
					setSelectedComponent(newPage.getComponent());
				}
			});
			component.setLocation(0,TAB_HEIGHT+GAP_HEIGHT);
			component.setSize(getWidth(),getHeight()-TAB_HEIGHT-GAP_HEIGHT);
			add(component);
			for (int i= index;i<pages.size();i++) {
				newPages.add(pages.get(i));
			}
			pages = newPages;

			if (pages.size() == 1) {
				setSelectedIndex(0);
			}
			else if (autoSelect) {
				setSelectedIndex(index);
			}
		}
		update();
	}

	/**
	 * Adds a right-click menu to the tab at the specified index
	 * @param index the index of the tab to which to add a menu
	 * @param menu the menu to add to the specified tab
	 */
	public void setTabMenu(int index,JPopupMenu menu) {
		pages.get(index).getTab().setMenu(menu);
	}

	/**
	 * @param index
	 * @return the component at the specified tab index
	 */
	public Component getComponentAt(int index) {
		if ((pages != null) && (index >=0) && (index < pages.size())) {
			return pages.get(index).getComponent();
		}
		return null;
	}

	/**
	 * @param index
	 * @return the component at the specified tab index
	 */
	public Component getTabAt(int index) {
		return getComponentAt(index);
	}

	/**
	 * This will attempt to find the component amongst the list of pages.
	 * 
	 * If the object does not exist, it will return -1.
	 * 
	 * @param c the component to look for
	 * @return the index of the component, -1 if it is inexistant
	 */
	public int getComponentIndex(Component c) {
		if (pages != null) {
			for (int i=0;i<pages.size();i++) {
				if (pages.get(i).getComponent().equals(c)) {
					return i;
				}
			}
		}
		return -1;
	}

	/**
	 * Returns the currently selected tab index
	 * @return an integer, -1 if nothing is selected.
	 */
	public int getSelectedIndex() {
		return selectedIndex;
	}

	/**
	 * @return the number of tabs that are currently open
	 */
	public int getTabCount() {
		return (pages != null) ? pages.size() : 0;
	}

	/**
	 * The painting method
	 */
	protected void paintComponent(Graphics g) {
		Graphics2D graphics = (Graphics2D) g;
		graphics.setColor(getBackground());
		graphics.fillRect(0,0,getWidth(),getHeight());
		graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
		graphics.setPaint(new GradientPaint(0,TAB_HEIGHT,GAP_TOP_COLOUR,0,TAB_HEIGHT+GAP_HEIGHT+1,GAP_BOTTOM_COLOUR, true));
		graphics.fillRect(0,TAB_HEIGHT,getWidth(),GAP_HEIGHT);
		graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
	}

	/**
	 * This is overridden so that components can be resized on all changes to the CustomTabPane's size
	 * @param x
	 * @param y
	 * @param w
	 * @param h
	 */
	@Deprecated
	public void reshape(int x, int y, int w, int h) {
		super.reshape(x, y, w, h);
		updateComponentSizes();
	}

	private void updateComponentSizes() {
		Dimension dim = new Dimension(getWidth(),getHeight()-TAB_HEIGHT-GAP_HEIGHT);
		for (TabPage tab : pages) {
			tab.getComponent().setSize(dim);
		}
	}

	/**
	 * Given a specific component, it's entire tab can be removed.
	 * 
	 * @param c
	 */
	public void removeTab(Component c) {
		removeTabAt(getComponentIndex(c));
	}

	/**
	 * This allows the removal of a tab at a specified index
	 * 
	 * @param index
	 */
	public void removeTabAt(int index) {
		if ((pages != null) && (index >=0) && (index < pages.size())) {
			TabPage page = pages.remove(index);
			remove(page.getTab());
			remove(page.getComponent());
			int tabWidth = page.getTab().getWidth()-OVERLAP;
			for (int i=index;i<pages.size();++i) {
				Tab tab = pages.get(i).getTab();
				tab.setLocation(tab.getX()-tabWidth,tab.getY());
			}
			totalTabLength -= tabWidth;
			if (index == selectedIndex) {
				if (pages.size()==0) {
					selectedIndex = -1;
				}
				else if (selectedIndex<pages.size()) {
					setSelectedIndex(selectedIndex);
				}
				else {
					setSelectedIndex(selectedIndex-1);
				}
			}
			else if (index<selectedIndex) {
				setSelectedIndex(selectedIndex-1);
			}
		}
		update();
	}

	public void removeAll() {
		while (!pages.isEmpty()) {
			removeTabAt(0);
		}
	}

	/**
	 * Internal method to select a new page.  This is not exposed to other classes.
	 * 
	 * @param index
	 */
	protected void select(int index) {
		if (selectedIndex>=0 && selectedIndex<pages.size()) {
			pages.get(selectedIndex).hide();
		}
		pages.get(index).show();
		selectedIndex = index;
	}

	/**
	 * This is how to select a page without knowing its index.
	 * 
	 * @param c
	 */
	public void setSelectedComponent(Component c) {
		int index = getComponentIndex(c);
		if (index != -1) {
			select(index);
		}
		else {
			throw new IllegalArgumentException("The component cannot be selected: it does not exist.");
		}
	}

	/**
	 * This is the method used to select a new tab.
	 * 
	 * @param newIndex
	 */
	public void setSelectedIndex(int newIndex) {
		if ((pages != null) && (newIndex >= 0) && (newIndex <= pages.size())) {
			select(newIndex);
		}
		else {
			throw new ArrayIndexOutOfBoundsException("Index out of range for tabs");
		}
	}

	/**
	 * This repaints the object.
	 */
	private void update() {
//		revalidate();
		repaint();
	}

	public void mouseWheelMoved(MouseWheelEvent e) {
		// Cap the scroll movement so that the tabs stay on the screen
		int change = Math.max(Math.min(e.getUnitsToScroll()*32,totalTabLength-getWidth()-xOffset),-xOffset);
		xOffset += change;
		for (int i=0;i<pages.size();++i) {
			Tab tab = pages.get(i).getTab();
			tab.setLocation(tab.getX()-change,tab.getY());
		}
		update();
	}

	/**
	 * This is the visual represenation of a Tab.
	 * 
	 * @author Joshua Tessier
	 * @author Neil Dickson
	 */
	private class Tab extends CustomButton {
		//TODO: Adjust tab height
		private	boolean		isSelected;
		private	String		name;
		private	ImageIcon	icon;
		private	JPopupMenu	menu;

		public Tab(String name, ImageIcon icon, int height, int width) {
			this.name = name;
			this.icon = icon;
			isSelected = false;
			menu = null;
			Dimension dim = new Dimension(width, height);
			setSize(dim);
			setMinimumSize(dim);
			setMaximumSize(dim);
			setPreferredSize(dim);
		}

		public void mouseClicked(MouseEvent e) {
			super.mouseClicked(e);
			if (e.getButton()==MouseEvent.BUTTON3 && menu!=null) {
				menu.show(this,e.getX(),e.getY());
			}
		}

		protected void paintComponent(Graphics g) {
			Graphics2D graphics = (Graphics2D) g;

			graphics.setColor(new Color(0,true));
			graphics.fillRect(0,0,getWidth(),getHeight());

			//We know the tab is of length 'width' and that the height in the middle
			//will not vary-- it has the regular tab shape.
			int[] xPoints = new int[LEFT_CURVE_Y.length + RIGHT_CURVE_Y.length];
			int[] yPoints = new int[LEFT_CURVE_Y.length + RIGHT_CURVE_Y.length];
			for (int x=0;x<LEFT_CURVE_Y.length;x++) {
				xPoints[x] = x;
				yPoints[x] = getHeight()-LEFT_CURVE_Y[x];
			}
			//We want to start these points at the far right end... so width - RIGHT_CURVE_Y.length
			//will give us the exact amount of points left to draw.
			int startPoint = getWidth() - RIGHT_CURVE_Y.length;
			for (int x=0;x<RIGHT_CURVE_Y.length;x++) {
				xPoints[x + LEFT_CURVE_Y.length] = startPoint + x;
				yPoints[x + LEFT_CURVE_Y.length] = getHeight()-RIGHT_CURVE_Y[x];
			}

			//We've now completed our polygon-- fill it.
			graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
			if (isSelected) {
				graphics.setPaint(new GradientPaint(0,getHeight(),SELECTED_BOTTOM_COLOUR,0,0,SELECTED_TOP_COLOUR));
			}
			else {
				graphics.setPaint(new GradientPaint(0,getHeight(),DESELECTED_BOTTOM_COLOUR,0,0,DESELECTED_TOP_COLOUR));
			}
			graphics.fillPolygon(xPoints, yPoints, xPoints.length);
			graphics.setColor(EDGE_COLOUR);
			graphics.drawPolyline(xPoints, yPoints, xPoints.length);
			graphics.setFont(FONT);
			graphics.setColor(FONT_COLOUR);
			graphics.drawString(name, ((icon!=null)?icon.getIconWidth():0)+LEFT_CURVE_Y.length, getHeight()-4);
			if (icon!=null) {
				graphics.drawImage(icon.getImage(),LEFT_CURVE_Y.length-3,3,null);
			}

			graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
		}

		public boolean isSelected() {
			return isSelected;
		}

		public void setSelected(boolean selected) {
			isSelected = selected;
			repaint();
		}

		public void setMenu(JPopupMenu menu) {
			this.menu = menu;
		}
	}

	private Tab createATab(String name,ImageIcon icon) {
		FontMetrics metrics = getFontMetrics(FONT);
		int length = Math.max(MIN_TAB_WIDTH,metrics.stringWidth(name) + LEFT_CURVE_Y.length+RIGHT_CURVE_Y.length + ((icon!=null)?icon.getIconWidth():0));
		Tab newTab = new Tab(name, icon, TAB_HEIGHT, length);
		newTab.setLocation(totalTabLength-xOffset, 0);

		totalTabLength += length - OVERLAP;

		add(newTab);

		return newTab;
	}

	/**
	 * TabPage is an internal class used to represent a tabbed-page.  It stores the name,
	 * the toolTip for the page, the actual component/file and the icon that represents it.
	 * 
	 * @author Joshua Tessier
	 */
	private class TabPage {
		private String name, toolTip;
		private Component component;
		private Icon icon;
		private Tab tab;

		public TabPage(String name, String toolTip, Component component, Icon icon, Tab tab) {
			this.name = name;
			this.toolTip = toolTip;
			this.component = component;
			this.icon = icon;
			this.tab = tab;
		}

		public Component getComponent() {
			return component;
		}

		public Icon getIcon() {
			return icon;
		}

		public String getName() {
			return name;
		}

		public Tab getTab() {
			return tab;
		}

		public String getToolTip() {
			return toolTip;
		}

		public void hide() {
			tab.setSelected(false);
			component.setVisible(false);
		}

		public void setTab(Tab tab) {
			this.tab = tab;
		}

		public void show() {
			tab.setSelected(true);
			CustomTabPane.this.setComponentZOrder(tab,0);
			component.setVisible(true);
		}
	}
}
